Skip to content

fix(objectql): a lax-admitted value withdraws the irreversible half of an ADR-0104 certificate (#4797) - #7285

Merged
os-zhuang merged 4 commits into
mainfrom
claude/issue-4797-lax-deviation-marker
Aug 10, 2026
Merged

fix(objectql): a lax-admitted value withdraws the irreversible half of an ADR-0104 certificate (#4797)#7285
os-zhuang merged 4 commits into
mainfrom
claude/issue-4797-lax-deviation-marker

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #4797. Implements the maintainer-approved conditional B ruling (2026-08-06, reaffirmed by triage 2026-08-08): a write let through by an operator escape hatch records a "deviation observed" marker, and that marker gates the irreversible paths while reversible behaviour continues.

The defect

recordDataMigrationRun documents its contract as:

verified_at is set on a passing run and CLEARED on a failing one — a deployment whose data has regressed since it last verified closes its own gate.

That only happened when a migration was re-run. Nothing told the ledger when the data actually regressed.

Normally nothing has to. Once sys_migration records a verified ADR-0104 migration the write path is strict, a non-conforming value is refused, and the certificate cannot go stale on its own. The operator escape hatches are the exception, and they exist precisely to relax a deployment that has already verified. With OS_ALLOW_LAX_MEDIA_VALUES / OS_ALLOW_LAX_VALUE_SHAPES on:

  1. a non-conforming value is admitted and persisted;
  2. sys_migration still reads verified_at non-null, blocking: 0;
  3. the switch goes off — or another process or machine runs without it — strict returns, and the same data starts being rejected;
  4. and the adr-0104-file-references row also governs reclamation of released field files, so the reap guard keeps deleting bytes on a certificate that is now false.

Same disease as #4769: the ledger's asserted fact was overturned by its own deployment, and the ledger does not know. #4794 deliberately covered only the empty-database half, because overturning a full-scan certificate on the strength of one write is the wrong order of magnitude.

The design

Producer. The engine's admit path — the same noteAdmittedValueShapeViolation sink that already tallies counterexamples for #4769 — stamps sys_migration.deviation_observed_at, plus a deviation_detail naming the object, field, type and parse issue. It reuses that path's shape deliberately: once per migration id per process, serialised, never awaited by the write, and it never inserts (no row means nothing was certified, so there is no authority to withdraw).

The marker does not clear verified_at. One admitted write is a complete disproof of "nothing here violates this contract", but it is not evidence of the same order as the full-store scan that earned the certificate. Revoking on it would turn an explicitly temporary switch into a one-way door — option A, rejected.

Consumer on the irreversible path. Authority is withdrawn in proportion to reversibility:

behaviour reversible? predicate while a deviation stands
strict value-shape enforcement (#3438) a rejected write is retried isDataMigrationFlagVerified continues
tombstoning a released file (#3459 PR-5b) lifted on re-attach isDataMigrationFlagVerified continues
reap guard's byte delete never authorisesIrreversibleAction refuses

storage-service-plugin.ts now supplies the reap guard's isCollectionOpen from the stronger mayActIrreversibly instead of isDataMigrationVerified. A closed gate vetoes but does not un-tombstone: the observed release stands, only the permission to delete is withheld, so the file is collected once the gate re-opens.

Clearing. A real os migrate files-to-references --apply / os migrate value-shapes --apply run walks the whole store again — evidence of the same order as the certificate — so recordDataMigrationRun clears the marker in both directions. invalidateDataMigrationFlags() also reopens the engine's once-per-process witness window, so a host that re-runs a migration in-process can deviate again and be seen.

Recording without gating was rejected for the opposite reason: a marker no code consumes is a declared-but-unenforced field (the ADR-0049 shape this repo is eliminating), and the bytes get deleted regardless.

Where the marker lives, and why

A dedicated pair of sys_migration columns, per migration id (matching the existing tally keying and the gate itself), contracted in packages/spec next to isDataMigrationFlagVerified. Not in the details JSON: that column is documented as diagnostics from the last run and is overwritten wholesale by each run, and a gate reading a diagnostics blob is the borrowed-evidence antipattern the ADR's addendum forbids one layer up. The row contract lives in spec so any package can read a flag without depending on platform-objects — a marker outside that contract could not be read by the consumer that needs it.

Cost measured: at most one ledger read+update per migration id per process, skipped once the marker stands. Pinned — five lax writes issue one update, not five.

Pins

packages/objectql/src/adr0104-lax-deviation-marker.test.ts (11) and packages/services/service-storage/src/lax-deviation-reclamation-gate.test.ts (8).

Positive: the marker is written and names the counterexample; verified_at and blocking survive; only the contradicted migration id is marked; strict enforcement still rejects once the hatch is off; the reap guard refuses the byte delete and reaps the same row after a re-run.

Negative — the foot-gun half, since a marker anything can set is not a safety device:

  • a dry-run validate() preview never marks (a preview that gates a later reclamation is a side effect);
  • a rejected write never marks (nothing was admitted);
  • a conforming write never marks, hatch or not;
  • never inserts a row when nothing was certified;
  • an already-unverified row is left alone;
  • attachment-scope tombstones still reap — they never rode on this flag;
  • a row written before these columns existed reads as "no deviation", so upgrading never retroactively closes a gate a deployment earned.

Backward compatibility

Additive. isDataMigrationFlagVerified is unchanged and keeps its existing consumers; authorisesIrreversibleAction (spec) and mayActIrreversibly (platform-objects) are the new stronger pair, and the reap guard is their one caller. Nothing is declared breaking, so no ADR-0087 disposition is required.

Related: #4769, #4794, ADR-0104.


Generated by Claude Code

…f an ADR-0104 certificate (#4797)

`recordDataMigrationRun` documents that a deployment whose data has regressed
since it last verified closes its own gate — but that only happened when a
migration was re-run. Nothing told the ledger when the data actually regressed.

Normally nothing has to: once `sys_migration` records a verified ADR-0104
migration the write path is strict and a non-conforming value cannot land. The
`OS_ALLOW_LAX_*` escape hatches are the exception, and they exist precisely to
relax a deployment that has already verified. With one on, the value is admitted
and persisted while the row still reads `verified_at` non-null, `blocking: 0` —
so strict rejects that same data the moment the switch goes off, and the reap
guard keeps deleting released field files on a certificate that is no longer
true.

A lax-admitted write now stamps `sys_migration.deviation_observed_at` (with a
`deviation_detail` naming the counterexample) on the migration whose contract it
broke, from the same admit-path sink that already tallies counterexamples for
 #4769. The marker does NOT clear `verified_at`: one admitted write is not
evidence of the same order as the full-store scan that earned the certificate,
and revoking on it would turn an explicitly temporary switch into a one-way
door.

Instead authority is withdrawn in proportion to reversibility. Strict
enforcement and tombstoning keep reading `isDataMigrationFlagVerified` and carry
on — a rejected write is retried, a tombstone is lifted on re-attach. The reap
guard's byte delete, which cannot be undone, now reads the stronger
`authorisesIrreversibleAction` / `mayActIrreversibly` and refuses while a
deviation stands. A real `os migrate … --apply` run walks the whole store again
and clears the marker.

Additive and backward compatible: a row written before these columns exist reads
as "no deviation observed", so upgrading never retroactively closes a gate a
deployment earned.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015KREcaEs85dphzUUqdYfX6
@vercel

vercel Bot commented Aug 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 10, 2026 5:23am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/objectql, @objectstack/platform-objects, @objectstack/service-storage, @objectstack/spec.

110 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/api/plugin-endpoints.mdx (via @objectstack/service-storage)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/objectql, packages/spec)
  • content/docs/concepts/north-star.mdx (via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via packages/objectql, @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/objectql)
  • content/docs/deployment/tenancy-modes.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/deployment/vercel.mdx (via @objectstack/objectql)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx (via packages/objectql, @objectstack/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/objectql, @objectstack/service-storage, @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/permissions/authentication.mdx (via @objectstack/objectql)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/permissions/system-context.mdx (via packages/objectql, packages/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/objectql, @objectstack/platform-objects, @objectstack/service-storage, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via packages/objectql, @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/apps.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/field-grouping-and-order.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/platform-objects, @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

7 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx (via @objectstack/objectql, @objectstack/service-storage, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

claude added 3 commits August 10, 2026 05:07
…mns (#4797)

`sys_migration` gained two fields, so the generated baselines follow: the spec
api-surface and export-origins snapshots (0 breaking, 2 added — the new
`authorisesIrreversibleAction` / `hasObservedDeviation` predicates), the
authorable-surface shard, the system reference page, and the four platform-object
translation bundles.

Also pins the new service-storage engine doubles to `assertEngineUpdateDispatch`,
so neither fake is looser than `ObjectQL.update` about what counts as a by-id
write (check:engine-double-contract).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015KREcaEs85dphzUUqdYfX6
…4797)

The fake is cast `as any` at the end, so its object-literal members were not
contextually typed and each `object` parameter landed on an implicit any —
three new TS7006s against a package whose type-check DEBT is a shrink-only
ratchet. Annotating them keeps the entry moving in the direction the ratchet
allows (52 recorded, 51 measured).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015KREcaEs85dphzUUqdYfX6
…diagnoses a deviation (#4797)

The guard is handed a boolean, so it cannot tell WHICH condition closed the
gate — and naming only "migration is not verified" became wrong the moment a
second condition existed. A deployment whose `verified_at` is plainly set would
be told its migration was never verified and sent hunting for a row that says
otherwise.

Both causes are now named, with the pointer to the two columns that distinguish
them. They share one remedy, so the instruction stays unambiguous either way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015KREcaEs85dphzUUqdYfX6
@claude

claude Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

PM note — the one red is a toolchain flake, not a build failure, and ⛔ not "pre-existing". Re-run triggered.

The session report called this "69/70 gates (1 pre-existing)". The direction is right — ⛔ nothing in this diff caused it — but the label matters operationally, so correcting it: "pre-existing" means leave it red; this one just needed a re-run.

Measured rather than assumed: Build Core is green on main HEAD and on every other open PR I sampled, so it is not failing repo-wide. The job log shows it died in 14 seconds, at pnpm --version, before install and before a single file compiled:

! Corepack is about to download https://registry.npmjs.org/pnpm/-/pnpm-10.31.0.tgz
AssertionError [ERR_ASSERTION]: The expression evaluated to a falsy value: assert(!this.paused)
    at Parser.finish (node:internal/deps/undici/undici:6157:9)

That is Corepack's fetch of pnpm crashing inside Node's bundled undici — infrastructure, not this branch. rerun_failed_jobs queued on run 31358440900.

Separately: the cross-seat declaration this PR needs is filed (#6017), since it adds two authorable fields to DataMigrationFlagSchema. ⛔ Staying draft until that veto window and the re-run both settle.

Two corrections in the session report are accepted and were worth making: the reclamation gate is createSysFileReapGuard in service-storage (wired at storage-service-plugin.ts:327), not packages/cli/src/commands/migrate/* — those commands have no reap or byte-delete path at all. My dispatch note listed cli as a likely site; it was wrong, and the dev measured it instead of following it.


Generated by Claude Code

@claude

claude Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Revising my own veto window, in the open, before acting on it — the premise I set it on does not hold.

At 05:47Z I declared PR #7285's authorable-surface touch and committed to ⛔ not flipping it ready until 07:50Z (a self-imposed 2 h). I am shortening that to now, and the reason is that I checked the precedent I was citing:

#5586 — the precedent this seat has been quoting all shift — was itself an authorable-surface touch. Its spec change was packages/spec/src/data/context-tokens.zod.ts, a grammar producer, and it was declared post-hoc and taken to ready + auto-merge in the same breath. I invented the stricter wait on the belief that the precedent covered only ledger-shaped touches and left authorable surfaces unaddressed. It does not — it addresses exactly this shape, and it says declare and proceed.

Reinforcing, not deciding: three lanes today independently used declare-and-proceed for packages/spec touches — engine-core (#7210), services (#7224), and domain:metadata on PR #7306 at 06:15Z, which states its terms as "default if silent: they land with #7306".

⚠️ What the veto still buys, so shortening it costs less than it looks: the window is not the last chance. This PR still has to clear the merge queue, and a packages/spec objection raised after ready is actionable right up to merge — I will pull it from the queue on request. ⛔ What I will not do is sit on a green PR past the point where the governing precedent says go, while two dispatchable cards (#7095, #7307) wait behind the same file.

Same shape as the call I made on PR #7261 earlier: the analysis was sound, one input was wrong, so I overturned the input in public rather than the analysis in silence. Recording it here rather than quietly flipping the PR, because a commitment other seats might rely on should not change without a reason attached.

Objection window stays open on #6017 and here.


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review August 10, 2026 06:42
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 10, 2026
Merged via the queue into main with commit 06be54e Aug 10, 2026
40 of 41 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-4797-lax-deviation-marker branch August 10, 2026 06:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:system size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(objectql): 扫描得来的 ADR-0104 证书在 lax 开关下会变陈旧 —— 数据回退了,闸却还开着

2 participants